package com.googlecode.connectlet.crypto.cipher.rc5;

import com.googlecode.connectlet.crypto.cipher.Decipheror;
import com.googlecode.connectlet.crypto.cipher.Encipheror;
import com.googlecode.connectlet.util.Bytes;

public class RC564Cipher implements Encipheror, Decipheror {
	private static final long serialVersionUID = 1L;

	public static final int DEFAULT_ROUNDS = 32;
	public static final int BLOCK_SIZE = 16;

	private static final long RC5_P64 = 0xb7e151628aed2a6bL;
	private static final long RC5_Q64 = 0x9e3779b97f4a7c15L;

	private static long rotl64(long a, int n) {
		return (a << n) | (a >>> (64 - n));
	}

	private static long rotr64(long a, int n) {
		return (a >>> n) | (a << (64 - n));
	}

	private long[] s;

	public RC564Cipher(byte[] key) {
		this(DEFAULT_ROUNDS, key);
	}

	public RC564Cipher(int rounds, byte[] key) {
		s = new long[rounds * 2 + 2];
		s[0] = RC5_P64;
		for (int i = 1; i < s.length; i ++) {
			s[i] = s[i - 1] + RC5_Q64;
		}

		long[] l = new long[key.length / 8];
		for (int i = 0; i < l.length; i ++) {
			l[i] = Bytes.toLong(key, i * 8, Bytes.LITTLE_ENDIAN);
		}

		long a = 0, b = 0;
		int steps = 3 * Math.max(l.length, s.length);
		for (int i = 0; i < steps; i ++) {
			a = s[i % s.length] = rotl64(s[i % s.length] + a + b, 3);
			b = l[i % l.length] = rotl64(l[i % l.length] + a + b, (int) (a + b) & 0x3f);
		}
	}

	@Override
	public int getBlockSize() {
		return BLOCK_SIZE;
	}

	@Override
	public void encrypt(byte[] src, int srcPos, byte[] dest, int destPos) {
		long a = Bytes.toLong(src, srcPos, Bytes.LITTLE_ENDIAN);
		long b = Bytes.toLong(src, srcPos + 8, Bytes.LITTLE_ENDIAN);
		a += s[0];
		b += s[1];
		for (int i = 2; i < s.length; i += 2) {
			a ^= b;
			a = rotl64(a, (int) b & 0x3f);
			a += s[i];
			b ^= a;
			b = rotl64(b, (int) a & 0x3f);
			b += s[i + 1];
		}
		Bytes.setLong(a, dest, destPos, Bytes.LITTLE_ENDIAN);
		Bytes.setLong(b, dest, destPos + 8, Bytes.LITTLE_ENDIAN);
	}

	@Override
	public void decrypt(byte[] src, int srcPos, byte[] dest, int destPos) {
		long a = Bytes.toLong(src, srcPos, Bytes.LITTLE_ENDIAN);
		long b = Bytes.toLong(src, srcPos + 8, Bytes.LITTLE_ENDIAN);
		for (int i = s.length - 2; i >= 2; i -= 2) {
			b -= s[i + 1];
			b = rotr64(b, (int) a & 0x3f);
			b ^= a;
			a -= s[i];
			a = rotr64(a, (int) b & 0x3f);
			a ^= b;
		}
		b -= s[1];
		a -= s[0];
		Bytes.setLong(a, dest, destPos, Bytes.LITTLE_ENDIAN);
		Bytes.setLong(b, dest, destPos + 8, Bytes.LITTLE_ENDIAN);
	}
}